﻿using Futaba.SBus.Extensions;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Futaba.SBus
{
	/// <summary>
	/// A background service which retrieves bus-data from the supplied <see cref="Readers.ISbusStreamReader"/>
	/// and parses it
	/// </summary>
	public class SbusParsingService : NotifyPropertyChangedBase, ISbusParsingService
	{
		bool _skipPackageTypeDetection = false;
		readonly BackgroundWorker _worker = new BackgroundWorker();

		#region ctor

		/// <summary>
		/// Creates a default instance using the default parsers
		/// </summary>
		public SbusParsingService()
		{
			InitializeWorker();
		}
		/// <summary>
		/// Creates an instance using the parsers supplied
		/// </summary>
		/// <param name="channelParser">the channel-parser to use</param>
		/// <param name="telemetryParser">the telemetry-parser to use</param>
		public SbusParsingService(Channels.IChannelParser channelParser, Telemetry.ITelemetryParser telemetryParser)
		{
			channelParser.EnsureNotNull("channelParser");
			telemetryParser.EnsureNotNull("telemetryParser");

			ChannelParser = channelParser;
			TelemetryParser = telemetryParser;
		}

		private void InitializeWorker()
		{
			_worker.WorkerSupportsCancellation = true;
			_worker.DoWork += WorkUntilCancellation;
#if DEBUG
			_worker.RunWorkerCompleted += (o, e) => Debug.WriteLine("Worker completed!");
#endif
		}

		#endregion

		#region events
		/// <summary>
		/// Raised when the <see cref="ISbusParsingService"/> is beginning to connect to the stream
		/// </summary>
		public event EventHandler Starting;

		private void RaiseStarting()
		{
			if (Starting == null)
				return;
			Starting(this, EventArgs.Empty);
		}
		/// <summary>
		/// Raised when the <see cref="ISbusParsingService"/> has connected to the stream and
		/// the parser is processing data
		/// </summary>
		public event EventHandler Started;

		private void RaiseStarted()
		{
			if (Started == null)
				return;
			Started(this, EventArgs.Empty);
		}
		/// <summary>
		/// Raised when the <see cref="ISbusParsingService"/> is beginning to disconnect from the stream
		/// </summary>
		public event EventHandler Stopping;

		private void RaiseStopping()
		{
			if (Stopping == null)
				return;
			Stopping(this, EventArgs.Empty);
		}
		/// <summary>
		/// Raised when the <see cref="ISbusParsingService"/> has disconnected from the stream
		/// and the parser has ended processing data
		/// </summary>
		public event EventHandler Stopped;

		private void RaiseStopped()
		{
			if (Stopped == null)
				return;
			Stopped(this, EventArgs.Empty);
		}
		#endregion

		#region properties

		private bool _internalIsRunning;
		/// <summary>
		/// Wether the service is currently running
		/// </summary>
		/// <seealso cref="Start"/>
		/// <seealso cref="Stop"/>
		public bool IsRunning
		{
			get { return _internalIsRunning; }
			protected set
			{
				if (Equals(_internalIsRunning, value))
					return;
				_internalIsRunning = value;
				RaisePropertyChanged(); //RaisePropertyChanged("IsRunning");
			}
		}

		private Telemetry.ITelemetryParser _internalTelemetryParser;
		/// <summary>
		/// The <see cref="Telemetry.ITelemetryParser"/> to use. If not specified, 
		/// the <see cref="Telemetry.TelemetryParser.DefaultInstance"/> is used instead
		/// </summary>
		public Telemetry.ITelemetryParser TelemetryParser
		{
			get { return _internalTelemetryParser ?? Telemetry.TelemetryParser.DefaultInstance; }
			protected set
			{
				if (Equals(_internalTelemetryParser, value))
					return;
				_internalTelemetryParser = value;
				UpdateTxTelemetryPassthroughMode();
				AssignReaderToParsers();
				RaisePropertyChanged(); //RaisePropertyChanged("TelemetryParser");
			}
		}

		private Channels.IChannelParser _internalChannelParser;
		/// <summary>
		/// The <see cref="Channels.IChannelParser"/> to use. If not specified, 
		/// the <see cref="Channels.ChannelParser.DefaultInstance"/> is used instead
		/// </summary>
		public Channels.IChannelParser ChannelParser
		{
			get { return _internalChannelParser ?? Channels.ChannelParser.DefaultInstance; }
			protected set
			{
				if (Equals(_internalChannelParser, value))
					return;
				_internalChannelParser = value;
				AssignReaderToParsers();
				RaisePropertyChanged(); //RaisePropertyChanged("ChannelParser");
			}
		}

		private Readers.ISbusStreamReader _internalReader;
		/// <summary>
		/// The reader which reads from the actual stream
		/// </summary>
		public Readers.ISbusStreamReader Reader
		{
			get { return _internalReader; }
			protected set
			{
				if (Equals(_internalReader, value))
					return;
				_internalReader = value;
				AssignReaderToParsers();
				RaisePropertyChanged(); //RaisePropertyChanged("Reader");
			}
		}

		private bool _internalChannelDataAvailable;
		/// <summary>
		/// Gets wether channel-information has been detected (or should be expected)
		/// </summary>
		/// <remarks>
		/// If data is not retrieved via the transmitter, channel-data is usually not available
		/// (unless you are reading the stream from the receiver's S.BUS port)
		/// </remarks>
		public bool ChannelDataAvailable
		{
			get { return _internalChannelDataAvailable; }
			protected internal set
			{
				if (Equals(_internalChannelDataAvailable, value))
					return;
				_internalChannelDataAvailable = value;

				UpdateTxTelemetryPassthroughMode();
				RaisePropertyChanged();
			}
		}

		private bool _internalTelemetryDataAvailable;
		/// <summary>
		/// Wether <see cref="Telemetry.Slot">Telemetry</see> data was detected
		/// </summary>
		public bool TelemetryDataAvailable
		{
			get { return _internalTelemetryDataAvailable; }
			protected internal set
			{
				if (Equals(_internalTelemetryDataAvailable, value))
					return;
				_internalTelemetryDataAvailable = value;
				RaisePropertyChanged();
			}
		}

		#endregion

		/// <summary>
		/// Starts the background-service
		/// </summary>
		/// <param name="reader">The <see cref="Readers.ISbusStreamReader">reader</see> to use</param>
		/// <param name="parseChannels">if set, specifies wether to expect channel-data from the stream</param>
		/// <param name="parseTelemetry">if set, specifies wether to expect telemetry-data from the stream</param>
		/// <exception cref="ArgumentNullException">Raised, if only <paramref name="parseChannels"/> 
		/// or <paramref name="parseTelemetry"/> is set, but not both</exception>
		/// <exception cref="ArgumentNullException">Raised, if the 
		/// ChannelParser.ItemsCollection or the TelemetryParser.ItemsCollection is null or empty</exception>
		/// <exception cref="InvalidOperationException">Raised, if the service is currently 
		/// running (<see cref="IsRunning"/>)</exception>
		/// <seealso cref="Starting"/>
		/// <seealso cref="Started"/>
		public void Start(Readers.ISbusStreamReader reader, bool? parseChannels = null, bool? parseTelemetry = null)
		{
			if (IsRunning)
				throw new InvalidOperationException("Service is running. Call Stop() first");

			reader.EnsureNotNull("reader");
			Reader = reader;
			ChannelParser.EnsureNotNull("ChannelParser");
			ChannelParser.ItemsCollection.EnsureNotEmpty("ChannelParser.ItemsCollection");
			TelemetryParser.EnsureNotNull("TelemetryParser");
			TelemetryParser.ItemsCollection.EnsureNotEmpty("TelemetryParser.ItemsCollection");

			if (parseChannels.GetValueOrDefault())
				ChannelParser.ItemsCollection.EnsureNotEmpty("ChannelParser.ItemsCollection");
			if (parseTelemetry.GetValueOrDefault())
				TelemetryParser.ItemsCollection.EnsureNotEmpty("TelemetryParser.ItemsCollection");

			_skipPackageTypeDetection = false;
			if (parseChannels.HasValue || parseTelemetry.HasValue)
			{
				if (!parseChannels.HasValue)
					throw new ArgumentNullException("parseChannels");
				if (!parseTelemetry.HasValue)
					throw new ArgumentNullException("parseTelemetry");

				if (!parseChannels.Value && !parseTelemetry.Value)
					throw new InvalidOperationException("parseChannels and parseTelemetry cannot both be false");

				ChannelDataAvailable = parseChannels.Value;
				TelemetryDataAvailable = parseTelemetry.Value;
				_skipPackageTypeDetection = true;
			}

			try
			{
				ChannelParser.Reset();
				TelemetryParser.Reset();
				IsRunning = true;
				RaiseStarting();
				Reader.OpenPort();
				_worker.RunWorkerAsync();
				RaiseStarted();
			}
			catch
			{
				IsRunning = false;
				throw;
			}
		}

		/// <summary>
		/// Stops the background-service
		/// </summary>
		/// <returns></returns>
		/// <seealso cref="Stopping"/>
		/// <seealso cref="Stopped"/>
		public async Task Stop()
		{
#if DEBUG
			Debug.WriteLine("Stopping worker...");
#endif
			RaiseStopping();
			_worker.CancelAsync();
			ChannelParser.CancellationPending = true;
			TelemetryParser.CancellationPending = true;
			await Task.Run(() =>
			{
				while (_worker.IsBusy)
				{
					Thread.Sleep(500);
#if DEBUG
					Debug.WriteLine("Worker is still busy! CancellationPending=" + _worker.CancellationPending);
#endif
				}
			});
#if DEBUG
			Debug.WriteLine("Worker stopped. Releasing port...");
#endif
			Reader.ReleasePort();
#if DEBUG
			Debug.WriteLine("All stopped.");
#endif
			IsRunning = false;
			RaiseStopped();
		}

		private void WorkUntilCancellation(object sender, DoWorkEventArgs e)
		{
			Thread.CurrentThread.Name = GetType().Name + "_BackgroundWorker";

			if (!_skipPackageTypeDetection)
				DetectPackageTypes();

			while (!_worker.CancellationPending)
			{
				if (ChannelDataAvailable)
					ChannelParser.ReadPacket();
				if (TelemetryDataAvailable)
					TelemetryParser.ReadPacket();
			}
		}

		private void DetectPackageTypes()
		{
			int bytesRead = -1;
			var data = new byte[100];

			ChannelDataAvailable = TelemetryDataAvailable = false;

			while (bytesRead < 99) //space for 2 complete messages
			{
				if (_worker.CancellationPending)
					return;
				var value = Reader.ReadByte();
				if (!value.HasValue)
					continue;
				bytesRead++;
				data[bytesRead] = value.Value;
			}

			ChannelDataAvailable = ChannelParser.DetectData(data);
			TelemetryDataAvailable = TelemetryParser.DetectData(data);
		}

		private void UpdateTxTelemetryPassthroughMode()
		{
			if (TelemetryParser != null)
				TelemetryParser.TxTelemetryPassthroughMode = ChannelDataAvailable;
		}

		private void AssignReaderToParsers()
		{
			ChannelParser.Reader = Reader;
			TelemetryParser.Reader = Reader;
		}

	}
}
